package c.c.d.s.s.o.ts.as.configuration;

import c.c.d.s.s.o.ts.as.configuration.support.client.CustomClientCredentialsTokenEndpointFilter;
import c.c.d.s.s.o.ts.as.configuration.support.token.CustomAuthorizationServerTokenServices;
import c.c.d.s.s.o.ts.as.configuration.support.token.CustomJwtAccessTokenConverter;
import c.c.d.s.s.o.ts.as.configuration.support.token.CustomTokenGranter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.endpoint.TokenKeyEndpoint;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;

/**
 * 授权服务器配置类<br>
 * {@code @EnableAuthorizationServer} 会启用 {@link org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint}
 * 和 {@link org.springframework.security.oauth2.provider.endpoint.TokenEndpoint} 端点.
 *
 * @author LiKe
 * @version 1.0.0
 * @date 2020-06-15 09:43
 * @see AuthorizationServerConfigurerAdapter
 */
@Slf4j
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    /**
     * Authorization Code Grant 的获取授权码的端点
     */
    public static final String OAUTH_AUTHORIZE_ENDPOINT = "/oauth/authorize";

    // ~ AuthorizationServerSecurityConfigurer configure
    // -----------------------------------------------------------------------------------------------------------------

    private AuthenticationEntryPoint authenticationEntryPoint;

    private AccessDeniedHandler accessDeniedHandler;

    private PasswordEncoder passwordEncoder;
    private ClientDetailsService clientDetailsService;

    // ~ ClientDetailsServiceConfigurer configure
    // -----------------------------------------------------------------------------------------------------------------
    private AuthenticationManager authenticationManager;
    private WebResponseExceptionTranslator webResponseExceptionTranslator;

    // ~ AuthorizationServerEndpointsConfigurer configure
    // -----------------------------------------------------------------------------------------------------------------
    private UserDetailsService userDetailsService;

    /**
     * Description: 授权服务器的安全配置, 主要是 {@code oauth/token} 端点.
     *
     * @see AuthorizationServerConfigurerAdapter#configure(AuthorizationServerSecurityConfigurer)
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        // @formatter:off
        // ~ 为 client_id 和 client_secret 开启表单验证, 会启用一个名为 ClientCredentialsTokenEndpointFilter 的过滤器.
        //   并会把这个过滤器放在 BasicAuthenticationFilter 之前,
        //   这样如果在 ClientCredentialsTokenEndpointFilter 完成了校验 (SecurityContextHolder.getContext().getAuthentication()),
        //   且请求头中即使有 Authorization: basic xxx, 也会被 BasicAuthenticationFilter 忽略.
        //   ref: AuthorizationServerSecurityConfigurer#clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter#doFilterInternal
        // ~ 如果不配置这一行, 默认就会通过 BasicAuthenticationFilter.
        // security.allowFormAuthenticationForClients();

        security
                // ~ Applied by AuthorizationServerSecurityConfiguration#configure(HttpSecurity)

                // ~ endpoint /oauth/token_key access control
                .tokenKeyAccess("permitAll()")
                // ~ endpoint /oauth/check_token access control, 供资源服务器校验令牌的端点 (如果采用的是 RemoteTokenServices)
                .checkTokenAccess("isAuthenticated()")

                // ~ ExceptionTranslationFilter handling
                //   在 Client Credentials Grant 和 Resource Owner Password Grant 模式下, 客户端凭证有误时会触发 authenticationEntryPoint
                //  ~ AuthenticationEntryPoint: called by ExceptionTranslationFilter when AuthenticationException be thrown.
                .authenticationEntryPoint(authenticationEntryPoint)
                //  ~ AccessDeniedHandler: called by ExceptionTranslationFilter when AccessDeniedException be thrown.
                .accessDeniedHandler(accessDeniedHandler)
                //  ~ 为 /oauth/token 端点 (TokenEndpoint) 添加自定义的过滤器
                .addTokenEndpointAuthenticationFilter(new CustomClientCredentialsTokenEndpointFilter(passwordEncoder, clientDetailsService, authenticationEntryPoint))
        ;
        // @formatter:on
    }

    /**
     * Description: 配置 {@link ClientDetailsService}
     *
     * @see AuthorizationServerConfigurerAdapter#configure(ClientDetailsServiceConfigurer)
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // @formatter:off
        clients.withClientDetails(clientDetailsService);
        // @formatter:on
    }

    /**
     * Description: 配置 {@link AuthorizationServerEndpointsConfigurer}<br>
     * Details: 配置授权服务器端点的非安全性特性, 例如 令牌存储, 自定义. 如果是密码授权, 需要在这里提供一个 {@link AuthenticationManager}
     *
     * @see AuthorizationServerConfigurerAdapter#configure(AuthorizationServerEndpointsConfigurer)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // @formatter:off
        // 对于密码授权模式, 需要提供 AuthenticationManager 用于用户信息的认证
        endpoints
                .authenticationManager(authenticationManager)

                // ~ 自定义的 WebResponseExceptionTranslator, 默认使用 DefaultWebResponseExceptionTranslator, 在 /oauth/token 端点
                //   ref: TokenEndpoint
                .exceptionTranslator(webResponseExceptionTranslator)

                // ~ 自定义的 TokenGranter
                .tokenGranter(new CustomTokenGranter(endpoints, authenticationManager))

                // ~ 自定义的 TokenStore
                .tokenStore(tokenStore())

                .tokenEnhancer(jwtAccessTokenConverter())

                // ~ 自定义的 AuthorizationServerTokenServices
                .tokenServices(new CustomAuthorizationServerTokenServices(endpoints))

                // ~ refresh_token required
                .userDetailsService(userDetailsService)
        ;
        // @formatter:on
    }

    /**
     * Description: 自定义 {@link JwtTokenStore}
     *
     * @return org.springframework.security.oauth2.provider.token.TokenStore {@link JwtTokenStore}
     * @author LiKe
     * @date 2020-07-20 18:11:25
     */
    private TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * Description: 为 {@link JwtTokenStore} 所须
     *
     * @return org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter
     * @author LiKe
     * @date 2020-07-20 18:04:48
     */
    private JwtAccessTokenConverter jwtAccessTokenConverter() {
        final KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("authorization-server.jks"), "SCLiKe11040218".toCharArray());
        final JwtAccessTokenConverter jwtAccessTokenConverter = new CustomJwtAccessTokenConverter();
        jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("authorization-server-jwt-keypair"));
        return jwtAccessTokenConverter;
    }

    @Bean
    public TokenKeyEndpoint tokenKeyEndpoint() {
        return new TokenKeyEndpoint(jwtAccessTokenConverter());
    }

    // ~ Autowired
    // -----------------------------------------------------------------------------------------------------------------

    @Autowired
    public void setAuthenticationEntryPoint(@Qualifier("customAuthenticationEntryPoint") AuthenticationEntryPoint authenticationEntryPoint) {
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    @Autowired
    public void setAccessDeniedHandler(@Qualifier("customAccessDeniedHandler") AccessDeniedHandler accessDeniedHandler) {
        this.accessDeniedHandler = accessDeniedHandler;
    }

    @Autowired
    public void setClientDetailsService(@Qualifier("customClientDetailsService") ClientDetailsService clientDetailsService) {
        this.clientDetailsService = clientDetailsService;
    }

    @Autowired
    public void setAuthenticationManager(@Qualifier("defaultAuthenticationManager") AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Autowired
    public void setWebResponseExceptionTranslator(@Qualifier("customWebResponseExceptionTranslator") WebResponseExceptionTranslator webResponseExceptionTranslator) {
        this.webResponseExceptionTranslator = webResponseExceptionTranslator;
    }

    @Autowired
    public void setUserDetailsService(@Qualifier("customUserDetailsService") UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Autowired
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }
}
